<template>
  <div class="node-create">
    <!-- 顶部返回条 -->
    <div class="custom-header">
      <span class="back-area" @click="goToNodeManagement">
        <svg class="back-icon" viewBox="0 0 16 16" width="18" height="18">
          <path d="M10.5 13 6 8l4.5-5" stroke="#bfbfbf" stroke-width="1.5" fill="none" stroke-linecap="round"
            stroke-linejoin="round" />
        </svg>
        <span class="back-text">返回</span>
      </span>
      <span class="header-divider">|</span>
      <span class="header-title">新增索引</span>
    </div>

    <!-- 页面主体 -->
    <div class="page-body">
      <!-- 步骤条 -->
      <div class="base-info">
        <StepHeader :current="currentStep" :nextDisabled="isNextDisabled" @prev="goToPreviousStep"
          @next="goToNextStep" />
      </div>

      <div class="step-wrapper">
        <!-- 基础信息（第 0 步） -->
        <div v-show="currentStep === 0">
          <BaseInfoSection :indexForm="indexForm" :remark="remark" :indexTypeOptions="indexTypeOptions"
            :indexTypeLoading="indexTypeLoading" :indexNameValidateStatus="indexNameValidateStatus"
            :indexNameHelp="indexNameHelp" :setIndexField="setIndexField" :setRemark="setRemark"
            @indexNameBlur="onIndexNameBlur" />
        </div>

        <!-- 生产源选择（第 1 步） -->
        <div v-show="currentStep === 1" class="prod-section">
          <ProdTableSection :dataSource="pagedData" :columns="columns" :prodLoading="prodLoading"
            :selectedId="selectedId" @rowSelect="onRowSelect" class="prod-table-wrap" />
        </div>

        <!-- 索引映射（第 2 步） -->
        <div v-show="currentStep === 2" class="mapping-section">
          <MappingSection :selectedSourceName="selectedSourceName" :strategyForm="strategyForm"
            :mappingData="mappingData" :mappingColumns="mappingColumns" :mappingLoading="mappingLoading"
            :inlineEditableCols="inlineEditableCols" :dropdownCols="dropdownCols" :dropdownOptions="dropdownOptions"
            :editing="editing" :editingValue="editingValue" :editingNumber="editingNumber"
            :mappingValidateStatus="mappingValidateStatus" :mappingValidateHelp="mappingValidateHelp" :addRow="addRow"
            :startEdit="startEdit" :onEditBlur="onEditBlur" :onEditPressEnter="onEditPressEnter"
            :onEditCancel="onEditCancel" :onDropdownSelect="onDropdownSelect" :setEditingNumber="setEditingNumber"
            :setEditingValue="setEditingValue" :setStrategyField="setStrategyField"
            :vectorMappingData="vectorMappingData" :vectorMappingColumns="vectorMappingColumns"
            :vectorInlineEditableCols="vectorInlineEditableCols" :vectorDropdownCols="vectorDropdownCols"
            :vectorDropdownOptions="vectorDropdownOptions" :addVectorRow="addVectorRow" />
        </div>

        <!-- 发布（第 3 步） -->
        <div v-show="currentStep === 3" class="publish-sectionBox">
          <PublishSection :saving="saving" @save="save" @cancel="cancel" />
        </div>

        <!-- 分页器 -->
        <div v-show="currentStep === 1" class="pager-dock" aria-label="分页器">
          <a-pagination :current="pagination.current" :pageSize="pagination.pageSize" :total="pagination.total"
            :pageSizeOptions="[10, 20, 50, 100]" :buildOptionText="pageSizeText" :show-total="showTotalText"
            show-size-changer @change="onPageChange" @showSizeChange="onPageSizeChange" />
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick, watch, h } from 'vue'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import Http from '@/api'

import StepHeader from './components/StepHeader.vue'
import BaseInfoSection from './components/BaseInfoSection.vue'
import ProdTableSection from './components/ProdTableSection.vue'
import MappingSection from './components/MappingSection.vue'
import PublishSection from './components/PublishSection.vue'

const router = useRouter()

/* 步骤 */
const currentStep = ref(0)
const saving = ref(false)

/* 基础信息 */
const indexForm = reactive({
  index_name: '',
  index_type: '',
  index_owner_group: '',
  index_permission: 'public',
})
const remark = ref('')
const setIndexField = (k: 'index_name' | 'index_type' | 'index_owner_group' | 'index_permission', v: any) => { ; (indexForm as any)[k] = v }
const setRemark = (v: string) => (remark.value = v)

const indexNameChecking = ref(false)
const indexNameExists = ref(false)
const indexNameLastChecked = ref<string>('')
let indexNameReqSeq = 0
watch(() => indexForm.index_name, () => (indexNameExists.value = false))
const indexNameValidateStatus = computed(() => (indexNameChecking.value ? 'validating' : indexNameExists.value ? 'error' : undefined))
const indexNameHelp = computed(() => (indexNameExists.value ? '索引名已存在，请重新输入' : ''))
const onIndexNameBlur = () => {
  const name = (indexForm.index_name || '').trim()
  if (!name) { indexNameChecking.value = false; indexNameExists.value = false; return }
  indexNameChecking.value = true
  checkIndexName(name)
}
async function requestIndexExists(name: string): Promise<boolean> {
  const url = `/api/data_service/indexes/exists/${encodeURIComponent(name)}`
  if (typeof (Http as any)?.get === 'function') {
    const res: any = await (Http as any).get(url)
    const d = res?.data ?? res
    if (typeof d === 'boolean') return d
    if (typeof d?.exists !== 'undefined') return !!d.exists
    if (typeof d?.exist !== 'undefined') return !!d.exist
    if (typeof d?.data !== 'undefined') return !!d.data
    return false
  }
  const resp = await fetch(url, { method: 'GET' })
  const d = await resp.json().catch(() => null)
  if (typeof d === 'boolean') return d
  if (typeof d?.exists !== 'undefined') return !!d.exists
  if (typeof d?.exist !== 'undefined') return !!d.exist
  if (typeof d?.data !== 'undefined') return !!d.data
  return false
}
async function checkIndexName(name: string) {
  const mySeq = ++indexNameReqSeq
  try {
    const exists = await requestIndexExists(name)
    if (mySeq !== indexNameReqSeq) return
    indexNameExists.value = !!exists
    indexNameLastChecked.value = name
    if (exists) { indexForm.index_name = ''; message.error('索引名已存在，请重新输入') }
  } finally { if (mySeq === indexNameReqSeq) indexNameChecking.value = false }
}

/* 索引类型字典 */
type Option = { label: string; value: string }
const indexTypeOptions = ref<Option[]>([])
const indexTypeLoading = ref(false)
const loadedIndexTypes = ref(false)
const indexTypesCache = ref<Option[] | null>(null)
const deepClone = <T>(x: T): T => JSON.parse(JSON.stringify(x))
const clearIndexTypesCache = () => (indexTypesCache.value = null)
async function loadIndexTypes() {
  if (loadedIndexTypes.value) return
  if (indexTypesCache.value?.length) { indexTypeOptions.value = deepClone(indexTypesCache.value); loadedIndexTypes.value = true; return }
  indexTypeLoading.value = true
  try {
    const res: any = await (Http as any).getIndexTypeDict()
    const list = Array.isArray(res?.data) ? res.data : []
    indexTypeOptions.value = list.map((it: any) => ({ label: String(it?.name ?? ''), value: String(it?.name ?? '') }))
    loadedIndexTypes.value = true
    indexTypesCache.value = deepClone(indexTypeOptions.value)
  } finally { indexTypeLoading.value = false }
}

/* 生产源列表 */
type ApiProdSource = {
  id: number; prod_source_name: string; data_count?: number | null; create_time?: string | null; update_time?: string | null;
  source?: string; source_channel?: string; channel?: string; prod_source_type?: string; source_name?: string;
  create_user?: string; creator?: string; create_by?: string; owner_name?: string; owner?: string; data_model?: string | null; pro_status?: string | number | null
}
type Row = {
  id: number; name: string; source: string; data_model: string; pro_status: string | number; dataCount: number;
  createTime: string; creator: string; createUser: string; updateTime: string
}

const prodLoading = ref(false)
const dataSource = ref<Row[]>([])
const selectedId = ref<number | null>(null)
const loadedProdSources = ref(false)

const prodCache = ref<{ list: Row[]; selectedId: number | null } | null>(null)
const writeProdCache = () => { prodCache.value = { list: deepClone(dataSource.value), selectedId: selectedId.value ?? null } }
const clearProdCache = () => (prodCache.value = null)

const columns = ref([
  { title: 'ID', dataIndex: 'id', key: 'id' },
  { title: '生产源名称', dataIndex: 'name', key: 'name', sorter: true },
  { title: '信源', dataIndex: 'source', key: 'source' },
  { title: '数据类型', dataIndex: 'data_model', key: 'data_model' },
  { title: '状态', dataIndex: 'pro_status', key: 'pro_status' },
  { title: '数据量', dataIndex: 'dataCount', key: 'dataCount', sorter: true },
  { title: '创建时间', dataIndex: 'createTime', key: 'createTime', sorter: true },
  { title: '更新时间', dataIndex: 'updateTime', key: 'updateTime', sorter: true },
  { title: '创建人', dataIndex: 'creator', key: 'creator' },
  { title: '操作', key: 'operation' },
])

const pagination = reactive({
  current: 1, pageSize: 10,
  get total() { return dataSource.value.length },
})
const selectedCount = computed(() => (selectedId.value == null ? 0 : 1))
const pageSizeText = (opt: any) => `${opt?.value ?? opt} 条/页`
const showTotalText = (t: number) => `已选择 ${selectedCount.value} 条，共 ${t} 条`

const pagedData = computed(() => {
  const start = (pagination.current - 1) * pagination.pageSize
  return dataSource.value.slice(start, start + pagination.pageSize)
})
const onPageChange = (page: number, pageSize: number) => { pagination.current = page; pagination.pageSize = pageSize }
const onPageSizeChange = (_: number, size: number) => { pagination.pageSize = size; pagination.current = 1 }

const onRowSelect = (id: number) => {
  selectedId.value = Number(id)
  writeProdCache()
  if (currentStep.value === 2 && Number.isFinite(selectedId.value)) {
    if (!loadMappingFromCache(selectedId.value!)) loadMappingBySource(selectedId.value!)
    if (!loadVectorMappingFromCache(selectedId.value!)) loadVectorMappingBySource(selectedId.value!)
  }
}
const selectedSourceName = computed(() => dataSource.value.find((r) => r.id === selectedId.value)?.name ?? '')

async function loadProdSources() {
  if (loadedProdSources.value) return
  if (prodCache.value) {
    dataSource.value = deepClone(prodCache.value.list)
    selectedId.value = prodCache.value.selectedId ?? selectedId.value
    loadedProdSources.value = true
    return
  }
  prodLoading.value = true
  try {
    const res: any = await (Http as any).getProdSource()
    const list: ApiProdSource[] = Array.isArray(res?.data) ? res.data : []
    const pickSource = (it: ApiProdSource) =>
      String((it as any).source ?? (it as any).source_channel ?? (it as any).channel ?? (it as any).prod_source_type ?? (it as any).source_name ?? '')
    const microCname =
      (window as any)?.__MicroAPP__?.user?.userInfo?.cname ||
      (window as any)?.__MicroAPP__?.__user?.userInfo?.cname || ''
    dataSource.value = list.map((it) => ({
      id: Number(it.id),
      name: String(it.prod_source_name ?? ''),
      source: pickSource(it),
      data_model: String(it.data_model ?? ''),
      pro_status: (it.pro_status ?? '') as any,
      dataCount: Number(it.data_count ?? 0),
      createTime: String(it.create_time ?? '-'),
      creator: microCname || String((it as any).creator ?? (it as any).create_user ?? (it as any).create_by ?? (it as any).owner_name ?? (it as any).owner ?? ''),
      createUser: microCname || String((it as any).creator ?? (it as any).create_user ?? (it as any).create_by ?? (it as any).owner_name ?? (it as any).owner ?? ''),
      updateTime: String(it.update_time ?? '-'),
    }))
    if (!selectedId.value && dataSource.value.length) selectedId.value = dataSource.value[0].id
    loadedProdSources.value = true
    writeProdCache()
  } catch (e: any) {
    message.error(e?.response?.data?.message || '生产源列表获取失败')
  } finally { prodLoading.value = false }
}

/* 倒排索引 Mapping */
type ApiMappingItem = {
  source_field_name?: string
  index_field_name: string
  source_field_type: string
  index_field_type: string
  if_index?: any
  if_store?: any
  tokenizer?: string
  tokenizer_dict_id?: string
  synonym_dict_id?: string
  permission?: number
  memo?: string
}

const toUIDataType = (t: string) => {
  switch ((t || '').toLowerCase()) {
    case 'string': return 'VARCHAR'
    case 'text': return 'TEXT'
    case 'int': return 'INT'
    case 'float': return 'FLOAT'
    case 'date': return 'DATE'
    case 'datetime': return 'DATE'
    case 'boolean': return 'BOOLEAN'
    case 'keyword': return 'Keyword'
    case 'longtext': return 'TEXT'
    default: return (t || '').toUpperCase()
  }
}
const toUIIndexType = (t: string) => {
  switch ((t || '').toLowerCase()) {
    case 'text': return 'TEXT'
    case 'keyword': return 'Keyword'
    case 'date': return 'DATE'
    case 'numeric': return 'NUMERIC'
    default: return (t || '').toUpperCase()
  }
}
const toYesNoString = (v: any) => (v === true || v === 'true' || v === 'Y' || v === 1 ? '是' : '否')
const toBoolString = (v: any) => (v === true || v === 'true' || v === 'Y' || v === 1 ? 'true' : 'false')

const mappingLoading = ref(false)
const mappingData = ref<any[]>([])
const renderIsIndexed = ({ text }: any) => h('span', { class: text === '是' ? 'indexed-yes' : 'indexed-no' }, text)

const mappingColumns = ref([
  { title: 'ID', dataIndex: 'id', key: 'id' },
  { title: '生产源字段名称', dataIndex: 'fieldName', key: 'fieldName' },
  { title: '数据类型', dataIndex: 'dataType', key: 'dataType' },
  { title: '是否索引', dataIndex: 'isIndexed', key: 'isIndexed', customRender: renderIsIndexed },
  { title: '索引字段名称', dataIndex: 'index_field_name', key: 'index_field_name' },
  { title: '索引字段类型', dataIndex: 'indexType', key: 'indexType' },
  { title: '是否存储', dataIndex: 'isStored', key: 'isStored' },
  { title: '分词器', dataIndex: 'tokenizer', key: 'tokenizer' },
  { title: '分词词典', dataIndex: 'tokenizerDict', key: 'tokenizerDict' },
  { title: '同义词词典', dataIndex: 'synonymDict', key: 'synonymDict' },
  { title: '内容转换规则', dataIndex: 'contentTransformRule', key: 'contentTransformRule' },
  { title: '权重', dataIndex: 'permission', key: 'permission' },
  { title: '备注', dataIndex: 'memo', key: 'memo' },
])

/* 倒排缓存 */
const mappingCache = ref<Record<string, any[]>>({})
const cacheKey = (sid: number | null) => (sid == null ? '' : String(sid))
const saveMappingToCache = () => { const key = cacheKey(selectedId.value); if (!key) return; mappingCache.value[key] = deepClone(mappingData.value) }
const loadMappingFromCache = (sid: number): boolean => {
  const key = cacheKey(sid); const cached = mappingCache.value[key]
  if (Array.isArray(cached)) { mappingData.value = deepClone(cached); renumberMappingIds(); mappingLoading.value = false; return true }
  return false
}
const renumberMappingIds = () => { mappingData.value.forEach((r, i) => (r.id = i + 1)) }

async function loadMappingBySource(sourceId: number) {
  if (!Number.isFinite(sourceId)) return
  mappingLoading.value = true
  try {
    const res: any = await (Http as any).getProdSourceMapping({ sourceId })
    const raw =
      (Array.isArray(res?.data) && res.data) ||
      (Array.isArray(res?.data?.index_mapping_items) && res.data.index_mapping_items) ||
      (Array.isArray(res?.index_mapping_items) && res.index_mapping_items) ||
      ([] as any[])

    const list: ApiMappingItem[] = Array.isArray(raw) ? raw : []

    mappingData.value = list.map((it, idx) => ({
      uid: `${sourceId}-${idx + 1}`,
      id: idx + 1,
      fieldName: it.source_field_name || '',
      index_field_name: it.index_field_name || '',
      dataType: toUIDataType(it.source_field_type),
      indexType: toUIIndexType(it.index_field_type),
      isIndexed: toYesNoString(it.if_index),
      isStored: toBoolString(it.if_store),
      tokenizer: it.tokenizer ?? '',
      tokenizerDict: it.tokenizer_dict_id ?? '',
      synonymDict: it.synonym_dict_id ?? '',
      contentTransformRule: '请选择',
      permission: typeof it.permission === 'number' ? it.permission : 100,
      memo: String((it as any).memo || ''),
    }))

    const fieldNames = Array.from(new Set(mappingData.value.map(r => String(r.fieldName || '')).filter(Boolean)))
    dropdownOptions.fieldName = fieldNames

    renumberMappingIds()
    saveMappingToCache()
    if (!list.length) message.info('该生产源暂无映射字段')
  } catch (e: any) {
    mappingData.value = []
    message.error(e?.response?.data?.message || '映射字段获取失败')
  } finally { mappingLoading.value = false }
}

/* 新增一行（倒排） */
const addRow = async () => {
  const sid = selectedId.value
  if (!Number.isFinite(Number(sid))) return message.error('请先在上一步选择一个生产源')
  mappingData.value.push({
    uid: `${sid}-extra-${mappingData.value.length + 1}`,
    id: mappingData.value.length + 1,
    fieldName: '', index_field_name: '', dataType: '', indexType: '',
    isIndexed: '否', isStored: '', tokenizer: '', tokenizerDict: '', synonymDict: '',
    contentTransformRule: '', permission: 0, memo: '',
  })
  renumberMappingIds()
  saveMappingToCache()
  await nextTick()
}

/* 内联编辑（共用） */
const inlineEditableCols = ['permission', 'index_field_name', 'memo'] as const
const editing = ref<{ id: string | null; field: string | null }>({ id: null, field: null })
const editingValue = ref<string>(''); const editingNumber = ref<number | null>(null)
let snapshot: { field?: string; value?: any } | null = null

const startEdit = (record: any, field: string) => {
  editing.value = { id: record.uid, field }
  snapshot = { field, value: (record as any)[field] }
  if (field === 'permission' || field === 'weight') {
    const n = Number(record[field]); editingNumber.value = Number.isFinite(n) ? Math.max(0, Math.min(100, Math.round(n))) : 0
  } else { editingValue.value = ((record as any)[field] ?? '') as string }
}
const onEditBlur = (record: any) => {
  if (!editing.value.field) return
  if (editing.value.field === 'permission' || editing.value.field === 'weight') {
    let n = Number(editingNumber.value); if (!Number.isFinite(n)) n = 0; n = Math.max(0, Math.min(100, Math.round(n)))
      ; (record as any)[editing.value.field] = n
  } else { ; (record as any)[editing.value.field] = (editingValue.value ?? '').trim() }
  saveRow(record); editing.value = { id: null, field: null }; snapshot = null
}
const onEditPressEnter = (record: any) => onEditBlur(record)
const onEditCancel = () => {
  if (!editing.value.field || !snapshot) { editing.value = { id: null, field: null }; return }
  const row = mappingData.value.find((r) => r.uid === editing.value!.id) || vectorMappingData.value.find((r) => r.uid === editing.value!.id)
  if (row) (row as any)[snapshot.field as string] = snapshot.value
  editing.value = { id: null, field: null }; snapshot = null
}
const saveRow = (record: any) => {
  const isVector = vectorMappingData.value.some((r) => r.uid === record.uid)
  if (isVector) saveVectorMappingToCache()
  else saveMappingToCache()
}

/* 下拉列（倒排） */
const dropdownCols = ['fieldName', 'dataType', 'indexType', 'isIndexed', 'isStored', 'tokenizer', 'tokenizerDict', 'synonymDict', 'contentTransformRule'] as const
type DropdownKey = (typeof dropdownCols)[number]
const dropdownOptions: Record<string, string[]> = {
  fieldName: [],
  dataType: ['VARCHAR', 'TEXT', 'DATE', 'INT', 'FLOAT', 'BOOLEAN', 'Keyword'],
  indexType: ['TEXT', 'Keyword', 'DATE', 'NUMERIC'],
  isIndexed: ['是', '否'],
  isStored: ['true', 'false'],
  tokenizer: ['标准分词器', '智能分词器', '英文分词器', '不分词'],
  tokenizerDict: ['默认词典', '通用词典', '行业词典-军事', '行业词典-财经', '自定义词典A', '自定义词典B'],
  synonymDict: ['默认词库', '公共词库', '军事同义词', '财经同义词', '无'],
  contentTransformRule: ['请选择', 'HTML去标签', '小写化', '去停用词', '正则清洗'],
}
const onDropdownSelect = (record: any, field: string, value: string) => {
  if (record[field] !== value) { record[field] = value; saveRow(record) }
}

/* ====== 同步策略 UI 与状态（仅表单） ====== */
const strategyForm = reactive({ datasync_strategy: 'regex', datasync_paras: '', time_strategy: '' })
const setStrategyField = (key: 'datasync_strategy' | 'datasync_paras' | 'time_strategy', v: string) => { ; (strategyForm as any)[key] = v }
const setEditingNumber = (v: number | null) => (editingNumber.value = v)
const setEditingValue = (v: string) => (editingValue.value = v)

/* ====== Mapping 校验（倒排） ====== */
const REQUIRED_MAPPING_FIELDS = ['fieldName', 'index_field_name', 'dataType', 'indexType'] as const
const firstMappingError = computed(() => {
  if (!mappingData.value.length) return '请至少添加一条 Mapping 配置'
  for (let i = 0; i < mappingData.value.length; i++) {
    const row: any = mappingData.value[i]
    for (const key of REQUIRED_MAPPING_FIELDS) {
      const val = (row[key] ?? '').toString().trim()
      if (!val) {
        const labelMap: Record<string, string> = {
          fieldName: '字段名称', index_field_name: '索引字段名称', dataType: '数据类型', indexType: '索引类型',
        }
        return `第 ${i + 1} 行「${labelMap[key]}」未填写`
      }
    }
  }
  return ''
})
const mappingValidateMergedHelp = computed(() => firstMappingError.value)
const mappingIsValid = computed(() => !firstMappingError.value)
const mappingValidateStatus = computed(() => (mappingIsValid.value ? undefined : 'error'))
const mappingValidateHelp = computed(() => (mappingIsValid.value ? '' : mappingValidateMergedHelp.value))

/* ====== 向量索引 Mapping ====== */
const vectorMappingData = ref<any[]>([])
const vectorMappingColumns = ref([
  { title: 'ID', dataIndex: 'id', key: 'id' },
  { title: '生产源字段名称', dataIndex: 'fieldName', key: 'fieldName' },
  { title: '数据类型', dataIndex: 'dataType', key: 'dataType' },
  { title: '是否索引', dataIndex: 'isIndexed', key: 'isIndexed', customRender: renderIsIndexed },
  { title: '索引字段名称', dataIndex: 'index_field_name', key: 'index_field_name' },
  { title: '索引类型', dataIndex: 'indexType', key: 'indexType' },
  { title: '向量维度（d）', dataIndex: 'vectorDimension', key: 'vectorDimension' },
  { title: '向量化算法', dataIndex: 'vectorAlgorithm', key: 'vectorAlgorithm' },
  { title: '权重', dataIndex: 'weight', key: 'weight' },
  { title: '备注', dataIndex: 'memo', key: 'memo' },
])
const vectorInlineEditableCols = ['index_field_name', 'weight', 'memo', 'vectorDimension', 'vectorAlgorithm'] as const
const vectorDropdownCols = ['fieldName', 'dataType', 'isIndexed', 'indexType'] as const
const vectorDropdownOptions: Record<string, string[]> = {
  fieldName: [], dataType: ['VARCHAR', 'TEXT', 'DATE', 'INT', 'FLOAT', 'BOOLEAN', 'Keyword'],
  isIndexed: ['是', '否'], indexType: ['FlatIndex', 'IVF', 'HNSW', 'PQ'],
  vectorDimension: ['d=64', 'd=128', 'd=256', 'd=512', 'd=768', 'd=1024'],
  vectorAlgorithm: ['FAISS', 'HNSW', 'CLIP', 'OpenAI', 'SentenceTransformers'],
}
const renumberVectorIds = () => { vectorMappingData.value.forEach((r, i) => (r.id = i + 1)) }

async function loadVectorMappingBySource(sourceId: number) {
  try {
    let res: any = null
    if (typeof (Http as any).getProdVectorMapping === 'function') res = await (Http as any).getProdVectorMapping({ sourceId })
    else if (typeof (Http as any).getVectorMapping === 'function') res = await (Http as any).getVectorMapping({ sourceId })
    else if (typeof (Http as any).getProdSourceVectorMapping === 'function') res = await (Http as any).getProdSourceVectorMapping({ sourceId })
    else res = await (Http as any).getProdSourceMapping({ sourceId })

    const raw =
      (Array.isArray(res?.data) && res.data) ||
      (Array.isArray(res?.data?.vector_mapping_items) && res.data.vector_mapping_items) ||
      (Array.isArray(res?.data?.vector) && res.data.vector) ||
      (Array.isArray(res?.vector_mapping_items) && res.vector_mapping_items) ||
      (Array.isArray(res?.vector) && res.vector) ||
      []

    const list: any[] = Array.isArray(raw) ? raw : []

    vectorMappingData.value = list.map((it, idx) => ({
      uid: `${sourceId}-vec-${idx + 1}`,
      id: idx + 1,
      fieldName: it.source_field_name || '',
      dataType: toUIDataType(it.source_field_type || it.field_type || ''),
      isIndexed: toYesNoString(it.if_index),
      index_field_name: it.index_field_name || '',
      indexType: (it.index_field_type || it.indexType || '') as string,
      vectorDimension: it.vector_dimension ? `d=${Number(it.vector_dimension)}` : '',
      vectorAlgorithm: it.vector_algorithm || '',
      weight: Number.isFinite(Number(it.index_weight ?? it.weight)) ? Number(it.index_weight ?? it.weight) : 0,
      memo: String((it as any).memo || ''),
      field_if_mapping: (it as any)?.field_if_mapping || 'Y',
    }))

    renumberVectorIds()
    const fieldNamesV = Array.from(new Set(vectorMappingData.value.map(r => String(r.fieldName || '')).filter(Boolean)))
    vectorDropdownOptions.fieldName = fieldNamesV
    saveVectorMappingToCache()
  } catch {
    vectorMappingData.value = []; vectorDropdownOptions.fieldName = []
  }
}

const vectorMappingCache = ref<Record<string, any[]>>({})
const saveVectorMappingToCache = () => { const key = cacheKey(selectedId.value); if (key) vectorMappingCache.value[key] = deepClone(vectorMappingData.value) }
const loadVectorMappingFromCache = (sid: number): boolean => {
  const key = cacheKey(sid); const cached = vectorMappingCache.value[key]
  if (Array.isArray(cached)) {
    vectorMappingData.value = deepClone(cached); renumberVectorIds()
    const f = Array.from(new Set(vectorMappingData.value.map(r => String(r.fieldName || '')).filter(Boolean)))
    vectorDropdownOptions.fieldName = f; return true
  }
  return false
}

/* 复原：向量索引 Mapping 的新增行 */
const addVectorRow = async () => {
  const sid = selectedId.value
  if (!Number.isFinite(Number(sid))) return message.error('请先在上一步选择一个生产源')
  vectorMappingData.value.push({
    uid: `${sid}-vec-extra-${vectorMappingData.value.length + 1}`,
    id: vectorMappingData.value.length + 1,
    fieldName: '', dataType: '', isIndexed: '否', index_field_name: '', indexType: '',
    vectorDimension: '', vectorAlgorithm: '', weight: 0, memo: '', field_if_mapping: 'Y',
  })
  renumberVectorIds()
  saveVectorMappingToCache()
  await nextTick()
}

/* ================= 向量索引 Mapping 校验 ================= */
const REQUIRED_VECTOR_FIELDS = ['fieldName', 'dataType', 'isIndexed', 'index_field_name', 'indexType'] as const
const isBlank = (v: any) => {
  const s = String(v ?? '').trim()
  return !s || s === '—'
}
const vectorFirstError = computed(() => {
  // 0 行直接通过
  if (!vectorMappingData.value.length) return ''
  for (let i = 0; i < vectorMappingData.value.length; i++) {
    const row: any = vectorMappingData.value[i]
    // 这 5 个字段全部为空 => 当作未填写该行，直接跳过
    const allEmpty = REQUIRED_VECTOR_FIELDS.every(k => isBlank(row[k]))
    if (allEmpty) continue
    // 否则：仅校验这 5 项都必须有值
    for (const key of REQUIRED_VECTOR_FIELDS) {
      if (isBlank(row[key])) {
        const labelMap: Record<string, string> = {
          fieldName: '生产源字段名称',
          dataType: '数据类型',
          isIndexed: '是否索引',
          index_field_name: '索引字段名称',
          indexType: '索引类型',
        }
        return `向量映射第 ${i + 1} 行「${labelMap[key]}」未填写`
      }
    }
  }
  return ''
})
const vectorMappingIsValid = computed(() => !vectorFirstError.value)
/* ======================================================================================================== */

/* 提交/导航 */
const toYN = (v: any) => (v === true || v === 'true' || v === 'Y' || v === 1 || v === '是' ? 'Y' : 'N')
const mapSourceTypeToBackend = (t: string) => {
  switch ((t || '').toUpperCase()) {
    case 'VARCHAR': return 'varchar'
    case 'TEXT': return 'longtext'
    case 'INT': return 'int'
    case 'FLOAT': return 'float'
    case 'DATE': return 'datetime'
    case 'BOOLEAN': return 'boolean'
    case 'KEYWORD': return 'keyword'
    default: return (t || '').toLowerCase()
  }
}
const mapIndexFieldTypeToBackend = (t: string) => {
  switch ((t || '').toUpperCase()) {
    case 'TEXT': return 'text'
    case 'KEYWORD': return 'keyword'
    case 'DATE': return 'date'
    case 'NUMERIC': return 'numeric'
    default: return (t || '').toLowerCase()
  }
}

const buildFinalPayload = () => {
  const index_mapping_items = (mappingData.value || []).map((row: any) => ({
    index_field_no: '', index_id: '', index_field_name: (row.index_field_name || '').trim(),
    field_if_mapping: 'Y', content_rule: 'Y',
    source_field_name: (row.fieldName || '').trim(),
    source_field_type: mapSourceTypeToBackend(row.dataType),
    index_field_type: mapIndexFieldTypeToBackend(row.indexType),
    if_index: toYN(row.isIndexed), if_store: toYN(row.isStored),
    tokenizer: row.tokenizer ?? '', tokenizer_dict_id: row.tokenizerDict ?? '', synonym_dict_id: row.synonymDict ?? '',
    index_weight: Number.isFinite(Number(row.permission)) ? Number(row.permission) : 0,
    memo: row.memo ?? '', operator_id: '',
  }))

  const vector_mapping_items = (vectorMappingData.value || []).map((row: any) => {
    const dim = Number(String(row.vectorDimension || '').replace(/^d=/, ''))
    return {
      index_field_no: '', index_id: '',
      source_field_name: (row.fieldName || '').trim(),
      source_field_type: mapSourceTypeToBackend(row.dataType),
      if_index: toYN(row.isIndexed),
      index_field_name: (row.index_field_name || '').trim(),
      index_field_type: (row.indexType || '').trim(),
      vector_dimension: Number.isFinite(dim) ? dim : 0,
      vector_algorithm: String(row.vectorAlgorithm || ''),
      index_weight: Number.isFinite(Number(row.weight)) ? Number(row.weight) : 0,
      memo: row.memo ?? '',
    }
  })

  const selectedRow = dataSource.value.find((r) => r.id === selectedId.value) || ({} as Row)
  return {
    index_id: 0,
    index_name: (indexForm.index_name || '').trim(),
    index_owner_id: '',
    index_owner_group: indexForm.index_owner_group || '',
    index_type: indexForm.index_type || '',
    index_permission: indexForm.index_permission || '',
    memo: remark.value || '',
    source_id: selectedId.value != null ? String(selectedId.value) : '',
    datasync_strategy: strategyForm.datasync_strategy || '',
    datasync_paras: (strategyForm.datasync_paras || '').trim(),
    time_strategy: strategyForm.time_strategy || '',
    index_mapping_items,
    vector_mapping_items,
    source_channel: (selectedRow as any)?.source || '',
    source_creator: (selectedRow as any)?.creator || (selectedRow as any)?.createUser || '',
  }
}

/* 把“向量映射校验”并入下一步判定 */
const canProceedStep2 = computed(() => {
  const strategyOk =
    strategyForm.datasync_strategy &&
    (strategyForm.datasync_strategy !== 'regex' ? true : !!strategyForm.datasync_paras.trim())
  return !!(strategyOk && mappingIsValid.value && vectorMappingIsValid.value)
})
const isNextDisabled = computed(() => {
  if (currentStep.value === 0) return !indexForm.index_name.trim() || !indexForm.index_type || indexNameExists.value || indexNameChecking.value
  if (currentStep.value === 1) return selectedId.value === null
  if (currentStep.value === 2) return !canProceedStep2.value
  return false
})

const clearAllCaches = () => { clearIndexTypesCache(); clearProdCache(); mappingCache.value = {}; vectorMappingCache.value = {} }

/* 仅保留保存与取消 */
const save = async () => {
  if (saving.value) return
  if (!indexForm.index_name.trim()) return message.error('请输入索引名称（index_name）')
  if (selectedId.value === null) return message.error('请选择一个生产源')
  if (!strategyForm.datasync_strategy || (strategyForm.datasync_strategy === 'regex' && !strategyForm.datasync_paras.trim()))
    return message.error('请完善同步策略')
  if (!mappingIsValid.value) return message.error(firstMappingError.value || 'Mapping 配置不完整')
  if (!vectorMappingIsValid.value) return message.error(vectorFirstError.value || '向量 Mapping 配置不完整')

  const payload = buildFinalPayload()
  saving.value = true
  try {
    await (Http as any).getCreateIndex(payload)
    message.success('创建成功')
    clearAllCaches()
    router.push('/')
  } catch (err: any) {
    const status = err?.response?.status
    const msg = err?.response?.data?.message || (status ? `请求失败（HTTP ${status}）` : '网络异常或服务器不可达')
    message.error(msg)
  } finally { saving.value = false }
}
const cancel = () => {
  router.push('/')
}

async function ensureDataForStep(step: number) {
  if (step === 0) await loadIndexTypes()
  else if (step === 1) await loadProdSources()
  else if (step === 2) {
    await loadProdSources()
    strategyForm.datasync_strategy = 'regex'
    if (Number.isFinite(selectedId.value)) {
      if (!loadMappingFromCache(selectedId.value!)) await loadMappingBySource(selectedId.value!)
      if (!loadVectorMappingFromCache(selectedId.value!)) await loadVectorMappingBySource(selectedId.value!)
    } else message.warning('请先在上一步选择一个生产源')
  }
}
const goToNextStep = async () => {
  if (currentStep.value === 0 && (!indexForm.index_name.trim() || !indexForm.index_type)) return message.error('请完善基础信息')
  if (currentStep.value === 1 && selectedId.value === null) return message.error('请选择一个生产源')
  if (currentStep.value === 2) {
    if (!strategyForm.datasync_paras.trim()) return message.error('请输入同步策略传参')
    if (!mappingIsValid.value) return message.error(firstMappingError.value)
    if (!vectorMappingIsValid.value) return message.error(vectorFirstError.value)
  }
  if (currentStep.value < 3) { currentStep.value++; await nextTick(); await ensureDataForStep(currentStep.value) }
  else router.push('/publish')
}
const goToPreviousStep = async () => { if (currentStep.value > 0) currentStep.value--; await nextTick(); await ensureDataForStep(currentStep.value) }
const goToNodeManagement = () => router.push('/')

onMounted(async () => { await ensureDataForStep(0) })
</script>



<style scoped lang="less">
/* --- 变量 --- */
@primary: #1677ff;
@blue-weak: #e6f7ff;
@border: #e5e5e5;
@line-gray: #e5e6eb;
@icon-gray: #f2f3f5;
@text-dark: #262626;
@text-gray: #bfbfbf;
@bg-page: #f0f2f5;
@border-light: #f0f0f0;
@text-secondary: #595959;

/* ===== 页面总体 ===== */
.node-create {
  --page-padding-x: 0px;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  padding: 0 var(--page-padding-x) 24px;
  background: @bg-page;

  /* 顶部返回条 */
  .custom-header {
    flex: 0 0 56px;
    height: 56px;
    background: #fff;
    border-bottom: 1px solid @border-light;
    display: flex;
    align-items: center;
    padding: 0 16px;
    margin-left: calc(-1 * var(--page-padding-x));
    margin-right: calc(-1 * var(--page-padding-x));
    width: calc(100% + var(--page-padding-x) * 2);
  }

  .back-area {
    display: inline-flex;
    align-items: center;
    cursor: pointer;
    user-select: none;
    font-size: 16px;
    color: #8c8c8c;
    transition: color 0.2s;
    padding-right: 4px;

    &:hover .back-text,
    &:hover .back-icon {
      color: @primary;
      stroke: @primary;
    }
  }

  .back-icon {
    margin-right: 2px;
    color: #bfbfbf;
    stroke: #bfbfbf;
    margin-left: 14px;
  }

  .back-text {
    font-size: 14px;
    color: @text-secondary;
    margin-right: 8px;
    line-height: 100%;
  }

  .header-divider {
    width: 1px;
    height: 16px;
    background: #d9d9d9;
    display: inline-block;
    margin: 0 8px;
    font-size: 0;
  }

  .header-title {
    font-size: 16px;
    font-weight: 500;
    color: @text-dark;
    letter-spacing: 0;
    line-height: 100%;
    margin-left: 8px;
  }

  /* 页面主体 */
  .page-body {
    display: flex;
    flex-direction: column;
    gap: 0;
    padding: 0;
    flex: 1 1 auto;
    min-height: 0;
  }

  .base-info {
    padding: 0;
    background: #fff;
  }

  .base-info :deep(.ant-steps-label-vertical .ant-steps-item-content) {
    margin-top: 0 !important;
  }

  .step-wrapper {
    margin: 10px 10px 0;
    background: #ffffff;
    border-radius: 8px;
    padding: 24px 24px 0 24px;
    display: flex;
    flex-direction: column;
    gap: 16px;
    box-sizing: border-box;
    flex: 1 1 auto;
    min-height: 0;
  }

  .base-info,
  .form-section,
  .mapping-section,
  .publish-sectionBox {
    background: #fff;
    border-radius: 8px;
    margin: 10px 10px 0;
  }

  /* 基础信息表单 */
  .form-section {
    .form-container {
      :deep(.ant-form-vertical > .ant-form-item:nth-child(1)) {
        margin-bottom: 16px !important;
      }

      :deep(.ant-form-vertical > .ant-form-item:nth-child(2)) {
        margin-top: 0 !important;
      }

      :deep(.ant-form-item-with-help .ant-form-item-explain) {
        min-height: 0 !important;
      }
    }

    .remark-wrap {
      position: relative;
      width: 100%;

      :deep(textarea.ant-input) {
        min-height: 120px;
        resize: vertical;
        overflow: auto;
        padding-right: 56px;
        padding-bottom: 28px;
        border-color: @border;

        &::placeholder {
          color: @text-gray;
        }
      }

      .remark-count {
        position: absolute;
        right: 12px;
        bottom: 8px;
        font-size: 12px;
        color: @text-gray;
        pointer-events: none;
      }
    }
  }

  /* 生产源列表区域 */
  .prod-section {
    flex: 1 1 auto;
    min-height: 0;
  }

  .prod-table-wrap {
    width: 100%;
  }

  /* 右下角分页器 */
  .pager-dock {
    align-self: flex-end;
    margin: 60px 16px;
    background: transparent;
    border: 0;
    box-shadow: none;
    padding: 0;
    z-index: 1;
    max-width: calc(100% - 32px);
    white-space: nowrap;
    -webkit-overflow-scrolling: touch;
  }

  .node-create .pager-dock :deep(.ant-pagination) {
    display: inline-flex;
    flex-wrap: nowrap !important;
    white-space: nowrap;
  }

  .node-create .pager-dock :deep(.ant-pagination-item),
  .node-create .pager-dock :deep(.ant-pagination-options) {
    margin-top: 0;
  }

  /* ===== 表格行高 ===== */
  .node-create .prod-table :deep(.ant-table-thead > tr > th) {
    background: #f4f8fd !important;
    height: 40px !important;
    line-height: 40px !important;
    padding-top: 0 !important;
    padding-bottom: 0 !important;
  }

  :deep(.ant-table-thead > tr > th) {
    background: #f4f8fd !important;
    height: 40px !important;
    line-height: 40px !important;
    padding-top: 0 !important;
    padding-bottom: 0 !important;
  }

  :deep(.ant-table-tbody > tr > td) {
    height: 40px !important;
    line-height: 40px !important;
    padding-top: 0 !important;
    padding-bottom: 0 !important;
  }

  .node-create .prod-table :deep(.ant-table-tbody > tr:not(.ant-table-measure-row):not(.ant-table-row-measure) > td) {
    height: 40px !important;
    line-height: 40px !important;
    padding-top: 0 !important;
    padding-bottom: 0 !important;
  }

  :deep(.ant-table-tbody > tr.ant-table-measure-row),
  :deep(.ant-table-tbody > tr.ant-table-row-measure),
  :deep(.ant-table-tbody > tr[style*='height: 0px']) {
    display: none !important;
    height: 0 !important;
    overflow: hidden !important;
  }

  .node-create .prod-table :deep(.ant-table-tbody > tr.ant-table-measure-row),
  .node-create .prod-table :deep(.ant-table-tbody > tr.ant-table-row-measure),
  .node-create .prod-table :deep(.ant-table-tbody > tr[style*='height: 0px']) {
    display: none !important;
    height: 0 !important;
    overflow: hidden !important;
  }

  .node-create .prod-table :deep(.ant-table-tbody > tr.ant-table-measure-row > td),
  .node-create .prod-table :deep(.ant-table-tbody > tr.ant-table-row-measure > td),
  .node-create .prod-table :deep(.ant-table-tbody > tr[style*='height: 0px'] > td) {
    height: 0 !important;
    line-height: 0 !important;
    padding: 0 !important;
    border: 0 !important;
  }

  /* 发布面板 */
  .publish-sectionBox {
    height: calc(100vh - 300px);
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .publish-section {
    display: flex;
    justify-content: center;
    align-items: center;
  }

  .publish-content {
    text-align: center;
  }

  .publish-icon {
    margin-bottom: 16px;
  }

  .publish-message {
    font-size: 18px;
    font-weight: 500;
    color: @text-dark;
    margin-bottom: 24px;
  }

  .publish-actions {
    display: flex;
    justify-content: 中心;
    gap: 16px;
  }

  .action-button {
    width: 120px;
  }

  .publish-card {
    position: relative;
  }

  .saving-mask {
    position: absolute;
    inset: 0;
    background: rgba(255, 255, 255, 0.6);
    backdrop-filter: blur(2px);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 8px;
  }

  .saving-box {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 220px;
    min-height: 120px;
    background: #fff;
    border: 1px solid @border;
    border-radius: 12px;
    box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08);
  }
}

/* 「是否索引」颜色：是=绿色，否=黑色 */
:deep(.indexed-yes) {
  color: #52c41a;
}

:deep(.indexed-no) {
  color: #000000;
}
</style>
